/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include "pch.h"
#include <journal/app/keys.h>
#include <journal/common/utils.hpp>
#include <journal/app/constants.h>
#include <journal/views/page.h>
#include <journal/views/interfaces.h>
#include <journal/models/interfaces.h>
#include <journal/views/ink_surface.h>
#include <journal/views/photo.h>
#include <journal/views/quad_renderer.h>
#include <journal/views/directx/util.hpp>
#include <journal/views/test_fractal.hpp>
#include <journal/views/paper_sheet_node.hpp>
#include <baja/linear_interpolation.hpp>
#include <journal/views/shared.hpp>
#include <journal/views/winrt/util.hpp>
#include <journal/common/util.h>
#include <baja/stream/storage_file.h>
#include <baja/graphics/dx11.1/blend_states.hpp>

namespace journal { namespace view {

b::uint32 page::_typeToken = 0;

// notes
// page view needs to be async mostly because of the async calls it makes to the render thread service
// during initialization to create its own textures as well as when adding photos for example (photos are async)
page::page(
    const std::shared_ptr<model::ipage_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    ) : 
        _pageModel(model),
        _inputCapture(inputCapture),
        _rebuildCache(false),
        _settingsObserverCookie(-1),
        _pageObserverCookie(-1),
        _inkSurfaceObserverCookie(-1),
        _shouldRender(true),
        _isThumbnailDirty(false),
        _isThumbnailFileDirty(false),
        _savingThumbnail(false),
        _testAnimation(0),
        _state(state::none),
        _alpha(1.f),
        _selected(false),
        _progressAnimation(0),
        _isThumbnailOutOfDate(false)
{
    static b::uint32 typeToken = constants::memoryTrackingService()->registerType(L"page view", true);
    if (_typeToken == 0)
    {
        _typeToken = typeToken;
    }
    constants::memoryTrackingService()->logCreate(_typeToken, 0, sizeof(page));
}

std::shared_ptr<page> page::create(
    const std::shared_ptr<model::ipage_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    )
{
    std::shared_ptr<page> returnValue = std::make_shared<page>(model, inputCapture);
    returnValue->init();
    return returnValue;
}

page::~page()
{
    this->cleanup();
    constants::memoryTrackingService()->logDispose(_typeToken, 0, sizeof(page));
}

void page::init()
{
    // observe the model
    _pageObserverCookie = _pageModel->addObserver(
        std::dynamic_pointer_cast<model::ipage_model_observer>(shared_from_this()));

    // create billboad node for page and add it to the scene
    _pageNode = paper_sheet_node::create(
        constants::graphics(),
        static_cast<b::uint32>(constants::general::pageWorldDimensions().x / constants::general::pageWorldGridWidth()) + 1,
        static_cast<b::uint32>(constants::general::pageWorldDimensions().y / constants::general::pageWorldGridWidth()) + 1,
        constants::general::pageWorldGridWidth()
        );

    _pageNode->setPosition(m::vector3d(0, 0, 0.f));
    _pageNode->setDefaultEffect(g::standard_effect::meshPhong);

    std::shared_ptr<g::imaterial> material = _pageNode->getMaterial();
    material->setSpecularPower(FLT_MAX); // matte
    material->setEmissiveColor(g::packColor(255, 255, 255, 255));
    material->setDiffuseColor(g::packColor(255, 100, 100, 100));

    // create the blank thumbnail texture
    _thumbnailTexture = constants::graphics()->createTexture(
        static_cast<b::uint32>(constants::general::thumbnailPixelDimensions().x),
        static_cast<b::uint32>(constants::general::thumbnailPixelDimensions().y),
        constants::general::mipLevels(), // mip map level
        static_cast<b::uint32>(g::texture_usage::renderTarget) | static_cast<b::uint32>(g::texture_usage::genMipMaps),
        g::pixel_format::d32BitColorWithAlpha,
        g::memory_pool::default,
        1, // single sampled
        0
        );

    // create shadow effect
    std::shared_ptr<g::ieffect2> effect = constants::graphics()->getAssetManager()->getEffect(L"shadow_effect");
    if (!effect)
    {
        std::shared_ptr<g::effect2_description> desc;
        desc = std::make_shared<g::effect2_description>(L"shadow_effect", L"shadow_pixel_shader.cso", L"unlit_vertex_shader.cso");

        desc->blendStateDesc.push_back(g::state_description(L"blend_solid", baja::graphics::dx11_1::createSolidBlendState()));
        desc->samplerStateDesc.push_back(g::state_description(L"linear_sampler", dx11_1::createLinearSampler()));
        desc->textureDesc.push_back(g::texture_description(L"texture1", 0));

        effect = constants::graphics()->getAssetManager()->addEffect(constants::graphics(), desc);
    }

    _shadowEffect = effect;

    // create billboad node for page and add it to the scene
    _shadowNode = gs::createBillboardNode(
        constants::graphics(),
        constants::general::pageWorldDimensions().x,
        constants::general::pageWorldDimensions().y,
        1, 1
        );
    _shadowNode->setCustomEffect(_shadowEffect);
    _shadowNode->getMaterial()->setDiffuseColor(g::color(constants::general::shadowPackedColor()));

    // observe settings service
    _settingsObserverCookie = constants::settings()->addObserver(
        std::dynamic_pointer_cast<isettings_service_observer>(shared_from_this()));

    this->setLoadLevel(load_level::none);
}

void page::generateFractalInkAsync()
{
    concurrency::task<void>([&]()
    {
        fractal f(_pageModel->inkSurface());
        f.generate("f");
    });
}

std::shared_ptr<iink_surface> page::inkSurface()
{
    return _inkSurfaceView;
}

void page::onPointer(const a::pointer_args& args, void* context)
{
    BAJA_VERIFY(context);

    bool* handled = static_cast<bool*>(context);

    std::shared_ptr<g::iscene_transformer> transformer;
    {
        transformer = constants::graphics()->snapshotCurrentSceneTransforms();
    }

    page_pointer_context ppc;
    ppc.isHit = _pageNode->pixelHitTest(transformer, args.point, &ppc.localPoint);
    ppc.handled = false;

    // if one of our children is capturing the input, pass it down directly to it
    std::shared_ptr<a::iinput_observer> target = _capture.get();
    if (target)
    {
        target->onPointer(args, static_cast<void*>(&ppc));
    }
    else if (_state == state::loadedFull)
    {
        if (!ppc.isHit)
        {
            // if the page isn't hit don't even bother
            return;
        }

        // nothing capturing, see if any photos want to handle it
        // iterating backwards -- from Top to Bottom in terms of z-order
        for (size_t i = _photos.size(); i > 0; --i)
        {
            _photos[i - 1].photo->onPointer(args, static_cast<void*>(&ppc));
            if (ppc.handled) break;
        }
        if (!ppc.handled && _inkSurfaceView) 
        {
            std::lock_guard<std::recursive_mutex> _lock(_mutex);
            _inkSurfaceView->onPointer(args, static_cast<void*>(&ppc));
        }
    }

    *handled = ppc.handled;
}

void page::onGesture(const a::gesture_args& args, void* context)
{
    BAJA_VERIFY(context);
    bool* handled = static_cast<bool*>(context);

    page_pointer_context ppc;
    ppc.isHit = this->pixelHitTest(args.position, &ppc.localPoint);
    ppc.handled = false;

    // if one of our children is capturing the input, pass it down directly to it
    std::shared_ptr<a::iinput_observer> target = _capture.get();
    if (target)
    {
        target->onGesture(args, static_cast<void*>(&ppc));
    }
    else if (_state == state::loadedFull)
    {
        if (!ppc.isHit)
        {
            // if the page isn't hit don't even bother
            return;
        }

        // nothing capturing, see if any photos want to handle it
        // iterating backwards -- from Top to Bottom in terms of z-order
        for (size_t i = _photos.size(); i > 0; --i)
        {
            _photos[i - 1].photo->onGesture(args, static_cast<void*>(&ppc));
            if (ppc.handled) break;
        }

        if (!*handled && _inkSurfaceView)
        {
            std::lock_guard<std::recursive_mutex> _lock(_mutex);
            _inkSurfaceView->onGesture(args, static_cast<void*>(&ppc));
        }
    }
    
    *handled = ppc.handled;
}

void page::onKey(const a::key_args& args, void* context)
{
    BAJA_VERIFY(context);
    bool* handled = static_cast<bool*>(context);

    if (_state != state::loadedFull) 
    {
        return;
    }

    // if one of our children is capturing the input, pass it down directly to it
    std::shared_ptr<a::iinput_observer> target = _capture.get();
    if (target)
    {
        target->onKey(args, context);
    }
    else if (journal::app::key_registry::check(journal::app::key::testInk1, args.vk))
    {
        generateFractalInkAsync();
        *handled = true;
    }
    else if (journal::app::key_registry::check(journal::app::key::testAnimation1, args.vk))
    {
        if (_testAnimation != 0)
        {
            this->testAnimation(false);
            _testAnimation = 0;
        }
        else
        {
            _testAnimation = 1;
        }
        *handled = true;
    }
    else if (journal::app::key_registry::check(journal::app::key::testAnimation2, args.vk))
    {
        if (_testAnimation != 0)
        {
            this->testAnimation(false);
            _testAnimation = 0;
        }
        else
        {
            _testAnimation = 2;
        }
        *handled = true;
    }
    else if (journal::app::key_registry::check(journal::app::key::saveImage, args.vk))
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_PAGE_VIEW, boost::wformat(L"saving image to file, page %i") % _pageModel->pageNumber());
        winrt::saveImageAsync(this->imageName().c_str(), this->generateImage(), default_folder::pictures, nullptr);
        *handled = true;
    }
    else if (journal::app::key_registry::check(journal::app::key::redrawPage, args.vk))
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_INK, L"rebuilding cache");
        this->setRebuildCache(true);
        *handled = true;
    }
    else if (journal::app::key_registry::check(journal::app::key::smooth, args.vk))
    {
        std::shared_ptr<ibool_setting> setting = std::dynamic_pointer_cast<ibool_setting>(constants::settings()->findSetting(constants::ink::name::smooth));
        bool b = !setting->getValue();
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_INK, boost::wformat(L"toggling ink smoothing %s") % (b ? L"on" : L"off"));
        constants::settings()->setBool((int)constants::ink::id::smooth, b);
        this->rebuildRenderData();
        this->setRebuildCache(true);
        *handled = true;
    }
    else if (journal::app::key_registry::check(journal::app::key::smoothVectorized, args.vk))
    {
        std::shared_ptr<ibool_setting> setting = std::dynamic_pointer_cast<ibool_setting>(constants::settings()->findSetting(constants::ink::name::smoothVectorized));
        bool b = !setting->getValue();
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_INK, boost::wformat(L"toggling ink smoothing vectorizer %s") % (b ? L"on" : L"off"));
        constants::settings()->setBool((int)constants::ink::id::smoothVectorized, b);
        this->rebuildRenderData();
        this->setRebuildCache(true);
        *handled = true;
    }
}

bool page::pixelHitTest(const m::point2d& point, m::point3d* localPoint)
{
    std::shared_ptr<g::iscene_transformer> transformer;
    {
        transformer = constants::graphics()->snapshotCurrentSceneTransforms();
    }
    return _pageNode->pixelHitTest(transformer, point, localPoint);
}

void page::setLoadLevel(const load_level level)
{
    if (DEBUG_CONSOLE_IS_TAG_ENABLED(TRACE_TAG_PAGE_VIEW))
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_PAGE_VIEW, 
            boost::wformat(L"entering page::setLoadLevel, page: %i, enter state: %s, load_level: %s") %
            _pageModel->pageNumber() %
            this->toString(_state) %
            this->toString(level)
            );
    }

    // state machine implementation for load level changes

    state previousState = _state;
    bool handled = false;

    ////////////////////////////////
    // state = destructing
    ////////////////////////////////
    if (_state == state::destructing)
    {
        // if we are destructing the page don't do anything
        //this->releaseThumbnail();
        //this->releaseFull();
        _state = state::none;
        handled = true;
    }
    ////////////////////////////////
    // state = none
    ////////////////////////////////
    else if (_state == state::none)
    {
        if (level == load_level::full)
        {
            _pageModel->setLoadLevelAsync(model::page_level::page_thumbnail);
            _state = state::loadingThumbnailThenFull;
            handled = true;
        }
        else if (level == load_level::thumbnail || level == load_level::thumbnailOrFull)
        {
            _pageModel->setLoadLevelAsync(model::page_level::page_thumbnail);
            _state = state::loadingThumbnail;
            handled = true;
        }

		if (handled)
		{
			// use the missing thumbnail texture which will hopefully be replaced even before the user gets
			// to see it
			// TRACK: the alternative to this is to just not show the page until it's ready with some sort of thumb
			// (as the current implementation may create a flashing effect of the missing thumb for a few frames)
			// however that's more complicated than i'd like to do at this point.  this is because the visibility of 
			// the page is controlled by the notebook and note the page itself, so we'd have to have a way to call 
			// back into the notebook once it calls setLoadLevel (new onLoadLevelSet in a view::ipage_observer interface)
			// this is not that hard but for now we will keep it simpler
            _pageNode->getMaterial()->setTexture(this->missingThumbnailTexture());
		}
    }
    ////////////////////////////////
    // state = loading thumbnail
    ////////////////////////////////
    else if (_state == state::loadingThumbnail)
    {
        if (level == load_level::none || level == load_level::thumbnailOrNone)
        {
            // cancel the thumbnail load
            _pageModel->setLoadLevelAsync(model::page_level::page_none);
            _state = state::none;
            handled = true;
        }
        else if (level == load_level::thumbnail  || level == load_level::thumbnailOrFull)
        {
            // already happening, nothing to do
            handled = true;
        }
        else if (level == load_level::full)
        {
            // we are loading the thumbnail but we are now requested the full data, just switch states
            _state = state::loadingThumbnailThenFull;
            handled = true;
        }
    }
    ////////////////////////////////
    // state = loading thumbnail then full
    ////////////////////////////////
    else if (_state == state::loadingThumbnailThenFull)
    {
        if (level == load_level::none || level == load_level::thumbnailOrNone)
        {
            // cancel the thumbnail load
            _pageModel->setLoadLevelAsync(model::page_level::page_none);
            _state = state::none;
            handled = true;
        }
        else if (level == load_level::thumbnail  || level == load_level::thumbnailOrFull)
        {
            // we are loading the full data thumbnail but we are now requested just the thumbnail, just switch states
            _state = state::loadingThumbnail;
            handled = true;
        }
        else if (level == load_level::full)
        {
            // nothing to do
            handled = true;
        }
    }
    ////////////////////////////////
    // state = loadedFull
    ////////////////////////////////
    else if (_state == state::loadedFull)
    {
        if (level == load_level::none)
        {
            // save the thumbnail before downgrading
            DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
                boost::wformat(L"saving thumbnail for page %i because of downgrade to load_level::none") %
                _pageModel->pageNumber()
                );

            this->saveThumbnailAsync();

            this->releaseThumbnail();
            this->releaseFull();

            // unload the page model
            _pageModel->setLoadLevelAsync(model::page_level::page_none);

            _state = state::none;
            handled = true;
        }
        else if (level == load_level::thumbnail || level == load_level::thumbnailOrNone)
        {
            // save the thumbnail before downgrading
            DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
                boost::wformat(L"saving thumbnail for page %i because of downgrade to load_level::thumbnail or thumbnailOrNone") %
                _pageModel->pageNumber()
                );

            // save the thumbnail before downgrading
            this->saveThumbnailAsync();

            // if we are loaded we should have the thumbnail already, so just release the full data
            this->releaseFull();

            // set the thumbnail as the texture
            BAJA_VERIFY(_thumbnailRenderTarget);
            _pageNode->getMaterial()->setTexture(std::dynamic_pointer_cast<g::itexture>(_thumbnailRenderTarget->renderTarget()));

            // unload the page model
            _pageModel->setLoadLevelAsync(model::page_level::page_none);

            _state = state::loadedThumbnail;
            handled = true;
        }
    }
    ////////////////////////////////
    // state = loadedThumbnail
    ////////////////////////////////
    else if (_state == state::loadedThumbnail)
    {
        if (level == load_level::none)
        {
            this->releaseThumbnail();

            // unload the page model
            _pageModel->setLoadLevelAsync(model::page_level::page_none);

            _state = state::none;
            handled = true;
        }
        else if (level == load_level::full)
        {
            // kick off a full load
            this->initFull();
            _pageModel->setLoadLevelAsync(model::page_level::page_full);

            _state = state::loadingFull;
            handled = true;
        }
        else if (level == load_level::thumbnailOrFull)
        {
            // set the thumbnail as the texture
            BAJA_VERIFY(_thumbnailRenderTarget);
            _pageNode->getMaterial()->setTexture(std::dynamic_pointer_cast<g::itexture>(_thumbnailRenderTarget->renderTarget()));
        }
    }

    if (DEBUG_CONSOLE_IS_TAG_ENABLED(TRACE_TAG_PAGE_VIEW))
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_PAGE_VIEW, boost::wformat(
            L"page::setLoadLevel(), %spage: %i, enter state: %s, load_level: %s, exit state: %s") %
            (handled ? L"" : L"<unhandled> ") %
            _pageModel->pageNumber() %
            this->toString(previousState) %
            this->toString(level) %
            this->toString(_state)
            );
    }
}

// shadows are a total fake.  we don't really project anything to the XY plane, we just 
// use a shadow node and resize it and position it in a way that makes it look like it's a projection
void page::renderShadow(b::float32 x, b::float32 y, b::float32 scaleX = 1.f)
{
    if (!_pageNode->getIsVisible())
    {
        return;
    }

    m::point3d position;
    _pageNode->getWorldPosition(&position);
    position.x += x;
    position.y += y;
    position.z = -constants::general::floorDistance();

    // another thing we are faking ... the shadow casted for curled pages is just a thinner version
    // of the normal shadow.  good enough.
    b::float32 width = constants::general::pageWorldDimensions().x * scaleX * 1.02f;
    b::float32 diffWidth = max(0, constants::general::pageWorldDimensions().x - width);
    position.x -= diffWidth / 2.f;

    _shadowNode->setPosition(position);
    _shadowNode->setScale(m::vector3d(1.02f * scaleX, 1.02f, 1.02f));
    m::matrix44 rotation;
    _pageNode->getRotation(&rotation);
    _shadowNode->setRotation(rotation);

    static const m::matrix44 identity;
    _shadowNode->updateTransforms(identity, identity, false);

    _shadowNode->render(constants::graphics());
}

void page::onPageLoadLevelSet(const std::shared_ptr<model::ipage_model>& page, const model::page_level currentLevel)
{
    ACQUIRE_VIEW_LOCK_EXPLICIT;

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"** page::onPageLoadLevelSet() : page id = %i, page number  = %i : level %i...") %
        page->storageId() %
        page->pageNumber() %
        static_cast<b::uint32>(currentLevel)
        );

    state previousState = _state;
    bool handled = false;

    ////////////////////////////////
    // state = loading thumbnail then full
    ////////////////////////////////
    if (_state == state::loadingThumbnailThenFull)
    {
        if (currentLevel == model::page_level::page_thumbnail)
        {
            // grab the thumbnail then start the full load
            this->initThumbnail();
            
            // set the thumbnail as the page texture
            _pageNode->getMaterial()->setTexture(std::dynamic_pointer_cast<g::itexture>(_thumbnailRenderTarget->renderTarget()));

            // init the full data, this will be updated as the full page loads while the thumbnail is still shown
            this->initFull();

            _pageModel->setLoadLevelAsync(model::page_level::page_full);

            _state = state::loadingFull;
            handled = true;
        }
    }
    ////////////////////////////////
    // state = loading thumbnail
    ////////////////////////////////
    else if (_state == state::loadingThumbnail)
    {
        if (currentLevel == model::page_level::page_thumbnail)
        {
            // grab the thumbnail then start the full load
            this->initThumbnail();

            // set the thumbnail as the page texture
            _pageNode->getMaterial()->setTexture(std::dynamic_pointer_cast<g::itexture>(_thumbnailRenderTarget->renderTarget()));

            _state = state::loadedThumbnail;
            handled = true;
        }
    }
    ////////////////////////////////
    // state = loading full
    ////////////////////////////////
    else if (_state == state::loadingFull)
    {
        if (currentLevel == model::page_level::page_full)
        {
            DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_OTHER, boost::wformat(L"total strokes: %i") % _pageModel->inkSurface()->inkStrokes()->size());

            // First make sure to render the cache completely, including the photos (but not the ink because we are not fully loaded yet)
            {
                this->setRebuildCache(true);
                this->renderToCache();
            }

            // Render all the ink strokes into the cache in a deferred way

            // here we relinquish the view lock to let the main render thread go while we do the deferred rendering
            // this makes sure we don't lock up the input and rendering of the main scene while we do this work
            // this is very important for performance and the difference can be seen with a simple test:
            // create a page and add a couple of fractal weeds to it, this will make sure that rendering the ink will
            // be good and slow due to the large number of strokes
            // go into expanded mode in list view and move the page in and out of the view quickly.  you'll see that when you lift the finger
            // the fractal weeds eventually get drawn in full fidelity replacing the thumbnail image (this would be this code below executing).
            // right after you lift the finger, put it down right away and move the page.  because of the deferred rendering of the ink
            // happening on a separate thread and not locking the view mutex, the touch input won't be blocked and everything will still be fluid
            // the ink strokes will still be drawing in the background while the user continues to interact with the page.
            // now comment out the relinquish and re-acquire lines and run the app 
            // do the same test.  this time you'll see the app gets blocked when you try to move the pages after you lifted the finger and starts
            // drawing the ink.
            RELINQUISH_VIEW_LOCK;
            {
                // Acquire the deferred view lock.  This will make sure that only one thread at a time is accessing
                // the deferred graphics context.
                // We could have more than one thread if we supported more than one context but at the time, we don't
                ACQUIRE_VIEW_DEFERRED_LOCK;

                // locking this mutex will protect from the ink surface view being touched while we are having it render deferred
                // so it's !!IMPORTANT!! to place locks elsewhere in the code where the ink surface view is being used
                std::lock_guard<std::recursive_mutex> _lock(_mutex);
                _state = state::loadingFullDeferred;

                b::stopwatch sw;

                // 1 is the deferred context number 1.  we only support one now, but in the future we could support more and let more
                // pages be rendered at the same time
                g::snap_render_target snapRenderTarget(constants::graphics(), 1);
                _cacheRenderTarget->apply(constants::graphics(), 1);
                _inkSurfaceView->onRenderDeferred();
            
                constants::performanceService()->collect(performance_event::deferredRender, sw.getElapsedMilliseconds()); 
            }

            REACQUIRE_VIEW_LOCK;
            constants::graphics()->executeDeferred();

            // do a one-off copy of the cache into the composite
            // the photos load first so they should be in the cache already
            dx11_1::copyPageRectangle(
                std::dynamic_pointer_cast<g::itexture>(_cacheRenderTarget->renderTarget()),
                std::dynamic_pointer_cast<g::itexture>(_compositeRenderTarget->renderTarget()),
                common::fullPageBounds()
                );

            // regenerate the mips
            if (constants::general::mipLevels() > 1)
            {
                b::stopwatch sw;
                constants::graphics()->generateMips(std::dynamic_pointer_cast<g::itexture>(_compositeRenderTarget->renderTarget()));
                constants::performanceService()->collect(performance_event::mipMapping, sw.getElapsedMilliseconds()); 
            }

            // now set the ink composite texture in the page
            _pageNode->getMaterial()->setTexture(std::dynamic_pointer_cast<g::itexture>(_compositeRenderTarget->renderTarget()));

            // we are fully loaded now!
            _state = state::loadedFull;

			// TRACK: I've seen cases where is up to date is true yet the thumbnail is corrupted
			// we can probably repro this by injecting a crash in the middle of drawing a fractal weed
			// for now always update it
			if (_isThumbnailOutOfDate)
			{
                DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
                    boost::wformat(L"saving thumbnail for page %i after upgrade to load_state::full load because thumbnail is out of date") %
                    _pageModel->pageNumber()
                    );

				_isThumbnailDirty = true;
				_isThumbnailFileDirty = true;
				
				// after loading full kick off a thumbnail update (this will make sure to "fix" any missing thumbnails)
				this->updateThumbnail(true); // force update
				this->saveThumbnailAsync(); 

                _isThumbnailOutOfDate = false;
			}
            else
            {
                DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
                    boost::wformat(L"upgrading page %i to load_level::full, thumbnail up to date no need to update") %
                    _pageModel->pageNumber()
                    );
            }

            handled = true;
        }
    }

    if (handled)
    {
        _shouldRender = true;
    }

    if (DEBUG_CONSOLE_IS_TAG_ENABLED(TRACE_TAG_PAGE_VIEW))
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_PAGE_VIEW, boost::wformat(
            L"page::onPageLoadLevelSet(), %spage: %i, enter state: %s, model::load_level: %s, exit state: %s") %
            (handled ? L"" : L"<unhandled> ") %
            page->pageNumber() %
            this->toString(previousState) %
            this->toString(currentLevel) %
            this->toString(_state)
            );
    }

    //RELINQUISH_VIEW_LOCK;
}

void page::initFull()
{
    // the viewport for all of our render targets
    g::viewport viewport = common::fullPageViewport();

    // create cache render target
    std::shared_ptr<g::itexture> texture = constants::graphics()->createTexture(
        static_cast<b::uint32>(viewport.width),
        static_cast<b::uint32>(viewport.height),
        1, // mip levels
        static_cast<b::uint32>(g::texture_usage::renderTarget),
        g::pixel_format::d32BitColorWithAlpha,
        g::memory_pool::default,
        1, // msaaCount 
        0 // msaaQualityLevel
        );

    _cacheRenderTarget = std::make_shared<g::render_target>(viewport, texture, nullptr);

    // create single-sampled render target that will be the shader resource texture for the page
    texture = constants::graphics()->createTexture(
        static_cast<b::uint32>(constants::general::pagePixelDimensions().x),
        static_cast<b::uint32>(constants::general::pagePixelDimensions().y),
        constants::general::mipLevels(),
        static_cast<b::uint32>(g::texture_usage::renderTarget) | static_cast<b::uint32>(g::texture_usage::genMipMaps),
        g::pixel_format::d32BitColorWithAlpha,
        g::memory_pool::default,
        1,
        0
        );

    // create final composite render target
    _compositeRenderTarget = std::make_shared<g::render_target>(viewport, texture, nullptr);
}

void page::releaseFull()
{
    _compositeRenderTarget = nullptr;
    _cacheRenderTarget = nullptr;
    if (_inkSurfaceObserverCookie != -1)
    {
        _inkSurfaceView->removeObserver(_inkSurfaceObserverCookie);
        _inkSurfaceObserverCookie = -1;
    }
    _inkSurfaceView = nullptr;

    // remove photo observers 
    std::for_each(
        _photos.begin(),
        _photos.end(),
        [&](photo_holder& h)
    {
        if (h.observerCookie != -1)
        {
            h.photo->removeObserver(h.observerCookie);
            h.observerCookie = -1;
        }
    });

    _photos.clear();
}

void page::initThumbnail()
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS, boost::wformat(L"page::initThumbnail, page %i") % _pageModel->pageNumber());

    // the viewport for the render target
    g::viewport viewport = common::getViewport(constants::general::thumbnailPixelDimensions());

    // get the thumbnail bytes from the model
    bs::binary_blob blob;
    _pageModel->thumbnail(blob);
    std::shared_ptr<g::itexture> thumbnailTexture;

    if (_pageModel->hasLinkedData() == false)
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
            boost::wformat(L"page::initThumbnail, page %i: no data, using paper thumbnail") %
            this->model()->pageNumber()
            );
        thumbnailTexture = paperTypeToTexture(static_cast<paper_type>(_pageModel->paperType()));
    }
    else
    {
        if (blob._size == 0 || blob._buffer == nullptr)
        {
			DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
				boost::wformat(L"page::initThumbnail, page %i: thumbnail is empty, using missing thumbnail texture") %
				this->model()->pageNumber()
				);

			thumbnailTexture = missingThumbnailTexture();
            _isThumbnailOutOfDate = true;
        }
        else if (!_pageModel->isThumbnailUpToDate())
        {
			DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
				boost::wformat(L"page::initThumbnail, page %i: thumbnail not up to date, using missing thumbnail texture") %
				this->model()->pageNumber()
				);

			thumbnailTexture = missingThumbnailTexture();
            _isThumbnailOutOfDate = true;
        }
        else
        {
			DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
				boost::wformat(L"page::initThumbnail, page %i: using thumbnail from disk") %
				this->model()->pageNumber()
				);

			// we have a thumbnail coming from the model, so use that
            // create the render target directly from the blob
            thumbnailTexture = constants::graphics()->createTextureFromFileInMemory( 
                static_cast<b::memory*>(blob._buffer),
                blob._size,
                static_cast<b::uint32>(g::texture_usage::renderTarget) | static_cast<b::uint32>(g::texture_usage::genMipMaps),
                g::pixel_format::d32BitColorWithAlpha,
                g::memory_pool::default,
                g::mipmap_load::noMipmaps // we generate our own mips below
                );
        }
    }

    this->setThumbnail(thumbnailTexture);

    if (constants::general::mipLevels() > 1)
    {
        b::stopwatch sw;
        constants::graphics()->generateMips(std::dynamic_pointer_cast<g::itexture>(_thumbnailRenderTarget->renderTarget()));
        constants::performanceService()->collect(performance_event::mipMapping, sw.getElapsedMilliseconds()); 
    }
}

void page::setThumbnail(const std::shared_ptr<g::itexture>& sourceTexture)
{
    g::viewport viewport = common::getViewport(constants::general::thumbnailPixelDimensions());

    g::snap_render_target snapRenderTarget(constants::graphics());
    
    _thumbnailRenderTarget = std::make_shared<g::render_target>(viewport, _thumbnailTexture, nullptr);
        _thumbnailRenderTarget->apply(constants::graphics());

    constants::graphics()->setZBuffer(g::enabled_state::disabled);
    constants::graphics()->setCulling(g::cull_state::none);
    quad_renderer::singleton()->render(
        1.f, // alpha
        0.f, // angle
        constants::general::pageWorldDimensions(),  // target dimensions
        constants::general::pageWorldDimensions(), // dimensions
        m::point2d(0,0), // origin
        sourceTexture
        );
}

void page::releaseThumbnail()
{
    _thumbnailRenderTarget = nullptr;
}

// save the thumbnail back to the model
bool page::saveThumbnailAsync()
{
    if (_state != state::loadedFull)
    {
        return false;
    }

    if (_savingThumbnail) return false; // already saving the thumbnail

    // our thumbnail is not dirty so nothing to update
    if (!_isThumbnailFileDirty)
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
            boost::wformat(L"page - page number %i thumbnail not dirty no need to save.") %
            this->model()->pageNumber()
            );

        return false;
    }

    // tell the model that we are creating a thumbnail
    _pageModel->setThumbnailSnapshotPoint();

    // update the thumbnail in memory first
    this->updateThumbnail(); 

    _savingThumbnail = true;

    // First get the bytes off of the texture
    std::shared_ptr<g::itexture> texture = std::dynamic_pointer_cast<g::itexture>(_thumbnailRenderTarget->renderTarget());
    dx11_1::thumbnail_data td(texture, g::file_format::JPG);

    bs::binary_blob blob;
    blob.createAndCopy(static_cast<b::uint32>(td.count()), static_cast<const void*>(td.bytes()));

    // Now pass these bytes to the model
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
        boost::wformat(L"page - saving thumbnail for page number %i.") %
        this->model()->pageNumber()
        );

    _pageModel->saveThumbnailAsync(blob);

    return true;
}

bool page::isThumbnailSaved()
{
    return !_isThumbnailFileDirty;
}

std::shared_ptr<imemory_blob> page::generateImage(const m::point2d* d)
{
    std::shared_ptr<g::itexture> sourceTexture;
    const m::point2d* dimensions = d;

    m::point2d newDimensions;
    if (_compositeRenderTarget)
    {
        sourceTexture = std::dynamic_pointer_cast<g::itexture>(_compositeRenderTarget->renderTarget());
        if (!dimensions) 
        {
            // make sure to generate an image of the page using the current camera scale
            dimensions = &newDimensions;
            std::shared_ptr<icamera_service> camera = b::services::get<icamera_service>();

            newDimensions.x = camera->worldToPixel(constants::general::pageWorldDimensions().x); 
            newDimensions.y = camera->worldToPixel(constants::general::pageWorldDimensions().y); 
    }
    }
    else if (_thumbnailRenderTarget)
    {
        sourceTexture = std::dynamic_pointer_cast<g::itexture>(_thumbnailRenderTarget->renderTarget());
        if (!dimensions) dimensions = &constants::general::thumbnailPixelDimensions();
    }
    else
    {
        return nullptr; // pagen not even loaded
    }
    
    // create a single mip levl texture as a destination for the page image
    std::shared_ptr<g::itexture> texture = constants::graphics()->createTexture(
        static_cast<b::uint32>(dimensions->x),
        static_cast<b::uint32>(dimensions->y),
        1, // mip map level
        static_cast<b::uint32>(g::texture_usage::renderTarget),
        g::pixel_format::d32BitColorNoAlpha,
        g::memory_pool::default,
        1, // single sampled
        0
        );

    g::viewport vp = common::getViewport(*dimensions);
    std::shared_ptr<g::render_target> renderTarget = std::make_shared<g::render_target>(vp, texture, nullptr);

    // Copy and downscale the current composite render target into the texture
    g::snap_render_target snapRenderTarget(constants::graphics());
    renderTarget->apply(constants::graphics());

    constants::graphics()->setZBuffer(g::enabled_state::disabled);
    constants::graphics()->setCulling(g::cull_state::none);

    quad_renderer::singleton()->render(
        1.f, // alpha
        0.f, // angle
        constants::general::pageWorldDimensions(),  // target dimensions
        constants::general::pageWorldDimensions(), // dimensions
        m::point2d(0,0), // origin
        sourceTexture
        );

    std::shared_ptr<dx11_1::thumbnail_data> td = std::make_shared<dx11_1::thumbnail_data>(texture, g::file_format::JPG);
    return std::dynamic_pointer_cast<imemory_blob>(td);
}

// update the thumbnail from the full blown data
bool page::updateThumbnail(const bool force)
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
        boost::wformat(L"page - updating thumbnail for page number %i. (forced = %s)") %
        this->model()->pageNumber() %
        (force ? L"true" : L"false")
        );

    // our thumbnail is not dirty so nothing to update
    if (!_isThumbnailDirty && !force)
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
            boost::wformat(L"page - page number %i thumbnail not dirty no need to update.") %
            this->model()->pageNumber()
            );

        return false;
    }

    // we are not fully loaded so not much we can update
    if (_state != state::loadedFull) 
    {
        DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
            boost::wformat(L"page - page number %i not fully loaded can't update thumbnail.") %
            this->model()->pageNumber()
            );

        return false;
    }

    BAJA_VERIFY(_compositeRenderTarget);
    BAJA_VERIFY(_thumbnailRenderTarget);

    // Copy and downscale the current composite render target into the thumbnail
    g::snap_render_target snapRenderTarget(constants::graphics());
    _thumbnailRenderTarget->apply(constants::graphics());

    constants::graphics()->setZBuffer(g::enabled_state::disabled);
    constants::graphics()->setCulling(g::cull_state::none);

    quad_renderer::singleton()->render(
        1.f, // alpha
        0.f, // angle
        constants::general::pageWorldDimensions(),  // target dimensions
        constants::general::pageWorldDimensions(), // dimensions
        m::point2d(0,0), // origin
        std::dynamic_pointer_cast<g::itexture>(_compositeRenderTarget->renderTarget())
        );

    if (constants::general::mipLevels() > 1)
    {
        b::stopwatch sw;
        constants::graphics()->generateMips(std::dynamic_pointer_cast<g::itexture>(_thumbnailRenderTarget->renderTarget()));
        constants::performanceService()->collect(performance_event::mipMapping, sw.getElapsedMilliseconds()); 
    }

    _isThumbnailDirty = false;

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_THUMBNAILS,
        boost::wformat(L"page - page number %i succesfuly updated thumbnail.") %
        this->model()->pageNumber()
        );

    return true;
}

bool page::shouldRebuildCache()
{
    if (_rebuildCache)
    {
        return true;
    }

    for (b::uint32 i = 0; i < _photos.size(); i++)
    {
        if (_photos[i].photo->shouldRebuildCache()) { return true;}
    }
    
    if (_inkSurfaceView)
    {
        return _inkSurfaceView->shouldRebuildCache();
    }

    return false;
}

void page::setRebuildCache(const bool value)
{
    _shouldRender = true;
    _rebuildCache = value;

    // Again, we are special casing the ink surface since we handle that during load in a special fashion 
    if (_inkSurfaceView && _state == state::loadedFull)
    {
        _inkSurfaceView->setRebuildCache(value);
    }

    for (b::uint32 i = 0; i < _photos.size(); i++) _photos[i].photo->setRebuildCache(value);
}

void page::rebuildRenderData()
{
    if (_inkSurfaceView && _state == state::loadedFull)
    {
        _inkSurfaceView->rebuildRenderData();
    }

    for (b::uint32 i = 0; i < _photos.size(); i++) _photos[i].photo->rebuildRenderData();
}

bool page::renderToCache()
{
    bool returnValue = false;

    {
        g::snap_render_target snapRenderTarget(constants::graphics());
        
        _cacheRenderTarget->apply(constants::graphics());

        if (this->shouldRebuildCache())
        {
            // make sure the rebuild is propagated to all the children
            this->setRebuildCache(true);

            constants::graphics()->setZBuffer(g::enabled_state::disabled);
            constants::graphics()->setCulling(g::cull_state::none);

            // write the paper texture into the cache first
            quad_renderer::singleton()->render(
                _alpha, // alpha
                0.f, // angle
                constants::general::pageWorldDimensions(),  // target dimensions
                constants::general::pageWorldDimensions(), // dimensions
                m::point2d(0,0), // origin
                paperTypeToTexture(static_cast<view::paper_type>(_pageModel->paperType()))
                );

            returnValue = true;
        }

        // render the photo's cached content
        for (b::uint32 i = 0; i < _photos.size(); i++) 
        {
            returnValue |= _photos[i].photo->renderToCache();
        }

        // now render the ink surface's cached content
        // we render the ink in a different way during load (in a deferred fashion), because it's more
        // expensive to render and we want to do that on a different thread.  Hence, we won't render the ink
        // surface unless the state is fully loaded
        if (_inkSurfaceView && _state == state::loadedFull)
        {
            returnValue |= _inkSurfaceView->renderToCache();
        }
    } // snap render target destroyed here

    // now copy the cache to the final composite
    if (returnValue)
    {
        dx11_1::copyPageRectangle(
            std::dynamic_pointer_cast<g::itexture>(_cacheRenderTarget->renderTarget()),
            std::dynamic_pointer_cast<g::itexture>(_compositeRenderTarget->renderTarget()),
            common::fullPageBounds()
            );
    }

    this->setRebuildCache(false);
    return returnValue;
}

bool page::getDynamicBounds(m::rectangle* bounds)
{
    // TRACK: right now the photo's dynamic bounds are the entire page this is why if there's
    // a photo with a non-empty dynamic bound, we just return
    // but this is a bad assumption to make here.
    // the right way to do this is to collect all the dynamic bounds and do the union
    for (b::uint32 i = 0; i < _photos.size(); i++)
    {
        if (_photos[i].photo->getDynamicBounds(bounds))
        {
            return true;
        }
    }

    if (_inkSurfaceView)	
    {
        return _inkSurfaceView->getDynamicBounds(bounds);
    }

    return false;
}

void page::renderDynamic()
{
    if (_inkSurfaceView)
    {
        _inkSurfaceView->renderDynamic();
    }

    for (b::uint32 i = 0; i < _photos.size(); i++) 
    {
        _photos[i].photo->renderDynamic();
    }
}

void page::setVisible(const bool visible)
{
    _pageNode->setIsVisible(visible);
}

bool page::visible()
{
    return _pageNode->getIsVisible();
}

void page::setPosition(const m::point3d& position)
{
    _pageNode->setPosition(position);
}

void page::setAmbientColor(const g::color& color)
{
    _pageNode->getMaterial()->setAmbientColor(color);
}

b::float32 page::alpha()
{
    g::color c = _pageNode->getMaterial()->getDiffuseColor();
    return c.a;
}

void page::setAlpha(const b::float32 value)
{
    g::color c = _pageNode->getMaterial()->getDiffuseColor();
    c.a = value;
    _pageNode->getMaterial()->setDiffuseColor(c);
}

// Just a helper so things look a little cleaner. Jorge, if you think this is confusing
// I'll rip it out.
void page::setCurling(const bool value)
{
    std::shared_ptr<paper_sheet_node> paperSheet;
    paperSheet = std::dynamic_pointer_cast<paper_sheet_node>(this->sceneNode());
    paperSheet->setCurling(value);
}

bool page::curling()
{
    std::shared_ptr<paper_sheet_node> paperSheet;
    paperSheet = std::dynamic_pointer_cast<paper_sheet_node>(this->sceneNode());
    return paperSheet->curling();
}

m::point3d page::rotation()
{
    return _rotation;
}

void page::setRotation(const m::point3d& angles)
{
    _rotation = angles;
    m::matrix44 rotation;
    rotation.setRotationXYZ(m::degToRad(angles.x), m::degToRad(angles.y), m::degToRad(angles.z));
    _pageNode->setRotation(rotation);
}

bool page::selected()
{
    return _selected;
}

void page::setSelected(const bool value)
{
    _selected = value;
}

m::point3d page::position()
{
    m::point3d position;
    _pageNode->getLocalPosition(&position);
    return position;
}

void page::testAnimation(const bool enable)
{
    std::shared_ptr<g::ivertex_buffer> vertexBuffer = _pageNode->vertexBuffer();
    
    g::vertex_lit_colored_textured* buffer = nullptr;
    g::vertex_lit_colored_textured* vertex = nullptr;

    vertexBuffer->lockNoOverwrite(
        0,
        _pageNode->vertexCountX() * _pageNode->vertexCountY(),
        (b::memory**)&buffer
        );

    b::uint32 x = 0;
    b::uint32 y = 0;

    double t = constants::general::currentTime();
    for (b::uint32 y = 0; y < _pageNode->vertexCountY(); y++)
    {
        for (b::uint32 x = 0; x < _pageNode->vertexCountX(); x++)
        {
            vertex = _pageNode->getVertex(buffer, x, y);
            if (enable)
            {
                b::float32 d = sqrt(vertex->position.x * vertex->position.x + vertex->position.y * vertex->position.y);
                b::float32 s =  static_cast<b::float32>(3*sin(1.5 * vertex->position.x + t/160.f));
                b::float32 c =  static_cast<b::float32>(3*cos(1.5 * vertex->position.y + t/200.f));

                b::float32 n = static_cast<b::float32>(sin(vertex->position.x + t/100.f) + sin(vertex->position.y + t/150.f));

                if (_testAnimation == 1)
                {
                    vertex->position.z = 1.f + n * 0.5f;
                }
                else
                {
                    vertex->position.z = 1.f + 0.3f * static_cast<b::float32>(1.5*sin(d*3 + t/400.f));
                }
            }
            else
            {
                vertex->position.z = 0.f; // reset back to flat
            }
        }
    }

    vertexBuffer->unlock();

    _pageNode->updateNormals();
}

void page::onRender()
{
    if (_state == state::deletingPhoto)
    {
        _progressAnimation.apply();

        b::float32 alpha = _progressAnimation.current();

        DEBUG_CONSOLE_TRACE(boost::wformat(L"%0.4f") % alpha);
        _selectedPhoto->setAlpha(alpha);
        _shouldRender = true;

        if (_progressAnimation.isFinished())
        {
            // mark the page to be re-rendered.
            this->setRebuildCache(true);
            _isThumbnailDirty = true;
            _isThumbnailFileDirty = true;

            std::vector<photo_holder>::iterator it = this->findPhoto(_selectedPhoto->model()->storageId());
            it->photo->beforeRemove();
            _photos.erase(it);

            _state = state::loadedFull;
        }
    }

    // test animation
    if (_testAnimation != 0)
    {
        this->testAnimation(true);
    }

    if (_state == state::loadedFull || _state == state::deletingPhoto)
    {
        // If we have the full data, render it
        if (!_shouldRender) return;

        // make sure to update our cache first
        this->renderToCache();

        m::rectangle bounds;
        if (this->getDynamicBounds(&bounds))
        {
            g::snap_render_target snapRenderTarget(constants::graphics());
                _compositeRenderTarget->apply(constants::graphics());

            dx11_1::copyPageRectangle(
                    std::dynamic_pointer_cast<g::itexture>(_cacheRenderTarget->renderTarget()),
                    std::dynamic_pointer_cast<g::itexture>(_compositeRenderTarget->renderTarget()),
                    bounds
                    );

                // now render the dynamic objects
                this->renderDynamic();
        }

        if (constants::general::mipLevels() > 1)
        {
            b::stopwatch sw;
            constants::graphics()->generateMips(std::dynamic_pointer_cast<g::itexture>(_compositeRenderTarget->renderTarget()));
            constants::performanceService()->collect(performance_event::mipMapping, sw.getElapsedMilliseconds()); 
        }

        _shouldRender = false;
    }
    else if (_state == state::loadedThumbnail)
    {
        BAJA_VERIFY(_thumbnailRenderTarget); // we should have at least the thumbnail
        // nothing to do really since the thumbnail texture itself is going to be the material already
    }
}

void page::onBeforeSettingChange(const b::wchar* fullName)
{
    if (std::wstring(fullName).compare(constants::ink::name::rebuildCache) == 0)
    {
        this->setRebuildCache(true);
    }
}

void page::onAfterSettingChange(const b::wchar* fullName)
{
    if (std::wstring(fullName).compare(constants::general::name::shadowPackedColor) == 0)
    {
        _shadowNode->getMaterial()->setDiffuseColor(g::color(constants::general::shadowPackedColor()));
    }
}

void page::onPageInitialized(const std::shared_ptr<model::ipage_model>& page)
{
    ACQUIRE_VIEW_LOCK;
    DEBUG_CONSOLE_TRACE(
        boost::wformat(L"  page::onPageInitialized(pageModel->storageId() = %i)") %
        page->storageId()
        );
}

void page::onPhotoModelAdded(const std::shared_ptr<model::iphoto_model>& photoModel)
{
    ACQUIRE_VIEW_LOCK;
    BAJA_VERIFY(photoModel);

    std::shared_ptr<iphoto> photo = view::createPhoto(
        photoModel, 
        std::static_pointer_cast<a::iinput_capture>(shared_from_this()));

    photo_holder w;
    w.photo = photo;
    w.observerCookie = photo->addObserver(std::dynamic_pointer_cast<iphoto_observer>(shared_from_this()));

    this->_photos.push_back(w);
    
    // during load, photos are rendered in a different way (see the onPageLoadLevelSet handler)
    if (_state == state::loadedFull)
    {
        this->setRebuildCache(true);
        _isThumbnailDirty = true;
        _isThumbnailFileDirty = true;
    }

    // raise photo added event
    _observerSource.raise(
        [&](const std::shared_ptr<view::ipage_observer>& observer, void* context)
        {
            observer->onPhotoAdded(photo);
        });
}

void page::onPhotoModelRemoved(const std::shared_ptr<model::iphoto_model>& photo)
{
    ACQUIRE_VIEW_LOCK;

    BAJA_VERIFY(_state == state::loadedFull);

    _progressAnimation.set(constants::general::currentTime(), constants::animations::quickDuration(), 1.f, 0.f);
    _state = state::deletingPhoto;
}

void page::onPhotoModelBroughtToFront(const std::shared_ptr<model::iphoto_model>& photo)
{
    ACQUIRE_VIEW_LOCK;
    BAJA_VERIFY(photo);

    std::vector<photo_holder>::iterator it = this->findPhoto(photo->storageId());
    photo_holder tempPhotoHolder = *it;
    _photos.erase(it);
    _photos.push_back(tempPhotoHolder);

    // mark the page to be re-rendered.
    this->setRebuildCache(true);
    _isThumbnailDirty = true;
    _isThumbnailFileDirty = true;
}

void page::onInkSurfaceModelReady(const std::shared_ptr<model::iink_surface_model>& inkSurfaceModel)
{
    ACQUIRE_VIEW_LOCK;
    BAJA_VERIFY(inkSurfaceModel);

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"** page::onInkSurfaceModelReady() : page id = %i, inksurface id = %i") %
        _pageModel->storageId() %
        inkSurfaceModel->storageId()
        );

    // create ink surface and bind it to the ink surface in the page model
    _inkSurfaceView = createInkSurface(
        inkSurfaceModel,
        std::static_pointer_cast<a::iinput_capture>(shared_from_this())
        );

    // observe ink surface
    _inkSurfaceObserverCookie = _inkSurfaceView->addObserver(
        std::dynamic_pointer_cast<iink_surface_observer>(shared_from_this()));
}

void page::onThumbnailSaved(const std::shared_ptr<model::ipage_model>& page)
{
    ACQUIRE_VIEW_LOCK;

    _isThumbnailFileDirty = false;
    _savingThumbnail = false;
    _observerSource.raise(
        [&](const std::shared_ptr<view::ipage_observer>& observer, void* context)
        {
            observer->onThumbnailSaved(shared_from_this());
        });
}

void page::onPageModelPaperTypeChanged(const b::uint32& paperType)
{
    ACQUIRE_VIEW_LOCK;

    // make everything dirty
    this->setRebuildCache(true);
    _isThumbnailDirty = true;
    _isThumbnailFileDirty = true;
}

std::vector<page::photo_holder>::iterator page::findPhoto(const model::id& id)
{
    auto it = std::find_if(
        _photos.begin(), 
        _photos.end(), 
        [&id](const photo_holder& p) 
        { 
            return p.photo->model()->storageId() == id; 
        }
    );

    return it;
}

const b::wchar* page::toString(const state s)
{
    if (s == state::none) return L"none";
    else if (s == state::loadingFull) return L"loadingFull";
    else if (s == state::loadingThumbnail) return L"loadingThumbnail";
    else if (s == state::loadingThumbnailThenFull) return L"loadingThumbnailThenFull";
    else if (s == state::loadedFull) return L"loadedFull";
    else if (s == state::loadedThumbnail) return L"loadedThumbnail";
    else return L"<unknown>";
}

const b::wchar* page::toString(const load_level ll)
{
    if (ll == load_level::full) return L"full";
    else if (ll == load_level::none) return L"none";
    else if (ll == load_level::thumbnail) return L"thumbnail";
    else if (ll == load_level::thumbnailOrNone) return L"thumbnailOrNone";
    else if (ll == load_level::thumbnailOrFull) return L"thumbnailOrFull";
    else return L"<unknown>";
}

const b::wchar* page::toString(const model::page_level ll)
{
    if (ll == model::page_level::page_full) return L"load_full";
    else if (ll == model::page_level::page_none) return L"load_none";
    else if (ll == model::page_level::page_self) return L"load_self";
    else if (ll == model::page_level::page_thumbnail) return L"load_thumbnail";
    else return L"<unknown>";
}

void page::cleanup()
{
    std::lock_guard<std::recursive_mutex> _lock(_mutex);

    if (!_inputCapture.expired())
    _inputCapture.lock()->releaseCapture();
    if (_pageModel) _pageModel->removeObserver(_pageObserverCookie);
    constants::settings()->removeObserver(_settingsObserverCookie);
    _settingsObserverCookie = -1;
    _pageNode = nullptr;
    _state = state::destructing;
    
    // this will release everything else
    this->setLoadLevel(load_level::none);
}

std::shared_ptr<ipage> createPage(
    const std::shared_ptr<model::ipage_model>& model,
    const std::shared_ptr<a::iinput_capture>& inputCapture
    )
{
    std::shared_ptr<page> page = page::create(model, inputCapture);
    return std::static_pointer_cast<ipage>(page);
}

void page::saveSharedImageAsync(const std::wstring& fileName, std::function<void(const std::shared_ptr<ishared_image>&)> f)
{
    // TRACK: we are generating a file during copy but this is not necessary, this is plainly because
    // I am being lazy and I just want to reuse the "share" code as is
    winrt::saveImageAsync(
        fileName.c_str(),
        this->generateImage(),
        default_folder::app,
        [f, fileName](const std::shared_ptr<imemory_blob>& blob)
        {
            // don't bother generating a thumbnail since we are sharing as a file we'll have winrt
            // generate a thumb from the file for us
            std::shared_ptr<shared_image> sharedImage = std::make_shared<shared_image>(
                nullptr, // thumbnail
                blob,
                fileName.c_str()
                );

            f(std::dynamic_pointer_cast<ishared_image>(sharedImage));
        });
}

void page::onShareImageAsync(copy_info& info, std::function<void(const std::shared_ptr<ishared_image>&)> f)
{
    this->saveSharedImageAsync(this->imageName(), f);
}

void page::onCopyImageAsync(copy_info& info, std::function<void(const std::shared_ptr<view::ishared_image>&)> f)
{
    this->saveSharedImageAsync(constants::general::sharedPageFileName(), f);
}

void page::onPasteImageAsync(const std::shared_ptr<view::ishared_image>& image, std::function<void()> f)
{
    std::shared_ptr<dx11_1::thumbnail_data> td = std::make_shared<dx11_1::thumbnail_data>(image->image(), g::file_format::JPG);

    // we need to generate unique names because the model has no way of knowing whether
    // the pasted image has the same contents or not
    std::wstring fileName = winrt::generateUniqueFileName(L"pasted_image", L"jpg");

    std::shared_ptr<model::ipage_model> pageModel = _pageModel;

    std::shared_ptr<view::page> pageView = std::dynamic_pointer_cast<view::page>(shared_from_this());

    winrt::saveImageAsync(
        fileName.c_str(),
        std::dynamic_pointer_cast<imemory_blob>(td),
        default_folder::app,
        [pageView, pageModel, fileName](std::shared_ptr<view::imemory_blob> blob)
    {
        std::shared_ptr<b::storage::istorage_file> imageFile = b::storage::createStorageFile();
        imageFile->initializeAsync(
            winrt::generateLocalFullFileName(fileName),
            [pageView, pageModel, imageFile](bool success)
        {
            pageView->addPhotoWithScale(imageFile);
        });
    });
}

void page::addPhotoWithScale(const std::shared_ptr<b::storage::istorage_file>& imageFile)
{
    addPhoto(
        imageFile,
        0.0f, // TRACK: 200: add photo at the right place
        0.0f,
        1.0f,
        0.0f);
}

void page::addPhoto(
    const std::shared_ptr<b::storage::istorage_file>& imageFile,
    const b::float32& x,
    const b::float32& y,
    const b::float32& scale,
    const b::float32& angle)
{
    std::shared_ptr<view::page> pageView = std::dynamic_pointer_cast<view::page>(shared_from_this());

    // what we are going to do here is crack open the file and get the width and height
    // so that we can calculate the scale
    imageFile->createMemoryStreamFromFileAsync(
    [=](bool success, const std::shared_ptr<b::imemory_stream>& ms)
    {
        if (!success)
        {
            BAJA_THROW(std::logic_error("an error occured while loading file content"));
        }

        std::shared_ptr<g::itexture> texture = 
            constants::graphics()->createTextureFromFileInMemory(
                (b::memory*)ms->getBuffer(),
                static_cast<uint32>(ms->getBufferSize()),
            0, // usage
            g::pixel_format::fromSource,
            g::memory_pool::default,
            g::mipmap_load::forceMipmaps
            );

        std::shared_ptr<icamera_service> camera = b::services::get<icamera_service>();

        // we are setting up the scale of the photo in such a way that at the current camera scale
        // it corresponds to the same number of pixels as the original photo
        b::float32 fitWidth = static_cast<b::float32>(texture->getWidth());
        b::float32 fitScale = (camera->pixelToWorld(fitWidth) / fitWidth);

        pageView->model()->addPhoto(
            imageFile,
            fitScale * scale,
            m::point2d(x, y),
            angle // angle
            );
    });
}

void page::onCommand(const view::command_id id, void* context)
{
    if (id == view::command_id::align_photo || id == view::command_id::ok_photo ||
        id == view::command_id::cancel_photo || id == view::command_id::bring_to_front_photo)
    {
        _selectedPhoto->onCommand(id, context);
        if (id == view::command_id::bring_to_front_photo)
        {
            auto photoIt = this->findPhoto(_selectedPhoto->model()->storageId());
            photoIt++;
            for (; photoIt != _photos.end(); photoIt++)
            {
                photoIt->photo->onSiblingPhotoSelected(false);
            }
            this->setRebuildCache(true);
        }
    }
}

void page::onSelectInkTool(ink_surface_tool tool)
{
    _inkSurfaceView->selectTool(tool);
}

void page::onRemoveSelectedPhoto()
{
    BAJA_ASSERT(_selectedPhoto != nullptr, L"a photo must be selected first!");

    _pageModel->removePhoto(_selectedPhoto->model()->storageId());
}

void page::onPhotoSelected(const std::shared_ptr<view::iphoto>& selectedPhoto)
{ 
    _selectedPhoto = selectedPhoto; 
    bool onTopOfSelectedPhoto = false;
    for (auto & photo : _photos) 
    {
        if (_selectedPhoto->model()->storageId() == photo.photo->model()->storageId())
        {
            onTopOfSelectedPhoto = true;
        }
        else
        {
            photo.photo->onSiblingPhotoSelected(onTopOfSelectedPhoto);
        }
    }
}

void page::onPhotoDeselected() 
{ 
    _selectedPhoto = nullptr; 
    for (auto &photo : _photos)
    {
        photo.photo->onSiblingPhotoDeselected();
    }
}

std::shared_ptr<g::itexture> page::paperTypeToTexture(const view::paper_type paperType)
{
    std::shared_ptr<g::iasset_manager> assetManager = constants::graphics()->getAssetManager();
    return assetManager->getOrCreateTextureFromFile(
        constants::graphics(),
        common::paperTypeToFileName(paperType),
        g::pixel_format::fromSource
        );
}

std::shared_ptr<g::itexture> page::missingThumbnailTexture()
{
    std::shared_ptr<g::iasset_manager> assetManager = constants::graphics()->getAssetManager();
    return assetManager->getOrCreateTextureFromFile(
        constants::graphics(),
        L"res\\missing_thumbnail.png",
        g::pixel_format::fromSource
        );
}

}} // namespace journal::view
